package org.wt.phoenix.middleware.generatorid.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.wt.phoenix.middleware.generatorid.config.IdSequenceConfig;
import org.wt.phoenix.middleware.generatorid.dao.IdSequenceDAO;
import org.wt.phoenix.middleware.generatorid.domain.IdSequence;
import org.wt.phoenix.middleware.generatorid.domain.IdSequenceQuery;
import org.wt.phoenix.middleware.generatorid.service.IdSequenceService;

import javax.annotation.Resource;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class IdSequenceServiceImpl implements IdSequenceService {

    private volatile static ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>> idCache = new ConcurrentHashMap<>();

    @Resource
    private IdSequenceDAO idSequenceDAO;

    @Resource
    private IdSequenceConfig idSequenceConfig;

    /**
     * 查本地缓存有无maxId，有则返回，无则查数据库、加锁、更新maxId
     *
     * @param businessTag
     * @return
     */
    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public Long nextId(String businessTag) {
        //build 查询入参
        IdSequenceQuery queryParam = new IdSequenceQuery().setBusinessTag(businessTag).setRetryNumber(new Integer(idSequenceConfig.getRetryNumber()));

        return tryGetId(queryParam);
    }

    /**
     * 尝试获取本地ID，高并发场景下会经常出现获取不到的情况
     * 如果获取不到，就要尝试重新刷新本地缓存
     *
     * @return
     */
    public Long tryGetLocalId(String businessTag) {
        ConcurrentLinkedQueue<Long> idCacheQueue = getLocalQueue(businessTag);
        if (!idCacheQueue.isEmpty()) {
            return idCacheQueue.poll();
        }
        return null;
    }

    /**
     * 获取本地队列
     * @param businessTag
     * @return
     */
    public static ConcurrentLinkedQueue<Long> getLocalQueue(String businessTag){
        ConcurrentLinkedQueue<Long> idCacheQueue = idCache.get(businessTag);
        if (idCacheQueue == null){
            synchronized (idCache) {
                idCacheQueue = idCache.get(businessTag);
                if (idCacheQueue == null) {
                    System.out.println("初始化了啊");
                    idCacheQueue = new ConcurrentLinkedQueue<>();
                    idCache.put(businessTag, idCacheQueue);
                }
            }
        }
        return idCacheQueue;
    }

    /**
     * 查询数据库,去除了悲观锁设计，仅保留retryNumber留作以后拓展
     * @param queryParam 查询参数
     * @return
     */
    private IdSequence getIdSequenceForDB(IdSequenceQuery queryParam){
        if (queryParam.getRetryNumber() > 0) {
            queryParam.setRetryNumber(queryParam.getRetryNumber() - 1);
        }
        return idSequenceDAO.findByBusinessTag(queryParam.getBusinessTag());
    }

    /**
     * 通过递归尝试获取ID
     *
     * @param queryParam 查询参数
     * @return
     */
    private Long tryGetId(IdSequenceQuery queryParam) {
        //尝试从本地获取，如果获取不到，就去DB查询刷新本地缓存
        Long id = tryGetLocalId(queryParam.getBusinessTag());
        if (id != null) {
            return id;
        }

        //查询数据库
        final IdSequence idSequence = getIdSequenceForDB(queryParam);

        //由于乐观锁有失败概率，所以查询完之后，可能有其他线程已经刷新了本地cache，所以再次尝试从本地获取
        id = tryGetLocalId(queryParam.getBusinessTag());
        if (id != null) {
            return id;
        }

        //尝试更新数据库
        final Long maxId = idSequence.getCurrentMaxId();
        Integer updateResult = idSequenceDAO.updateCurrentMaxId(maxId, queryParam.getBusinessTag());

        //如果更新成功，则刷新本地缓存
        if (updateResult > 0) {
            final long length = maxId + idSequence.getStep();
            ConcurrentLinkedQueue<Long> idCacheQueue = getLocalQueue(queryParam.getBusinessTag());

            for (AtomicLong genId = new AtomicLong(maxId + 1); genId.get() < length; genId.compareAndSet(genId.get(), genId.get() + 1)) {
                idCacheQueue.add(genId.get());
            }
            //去除了DCL设计，竞争乐观锁成功的线程，可预留一个id用作返回，避免了与其他线程竞争，所以可以不加锁了
            return maxId;
        }

        //重新尝试获取id
        do {
            id = tryGetId(queryParam);
        } while (id == null);

        return id;
    }
}
